package othello.client.playroom;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.slim3.datastore.Attribute;

import othello.shared.model.GameStep;

/**
 * <p>
 * 各プレイヤーのゲーム盤を表します。
 * </p>
 * 以下の情報を保持します。
 * <ul>
 * <li>ゲーム盤のID</li>
 * <li>プレイヤーのコマの色</li>
 * <li>現在の手番（白、黒）</li>
 * <li>現在のステップ数</li>
 * <li>現在の盤面の状態</li>
 * </ul>
 * 
 * 各プレイヤーごとにID、Sideが異なります。 対戦相手のプレイヤーとはIDが同じとなります。
 * 
 * @author onjiro.mohyahya
 */
public class GameBoard {

	/**
	 * エンクロージングクラスの変更を監視するインターフェース
	 */
	public interface Observer {
		void notifyUpdate(GameBoard updatedBoard);
	}

	/**
	 * 当ゲーム盤の識別ID
	 */
	private final String id;

	/**
	 * 自分のコマの色
	 */
	private final GameStep.Side mySide;

	/**
	 * 現在のステップでの手番
	 */
	private GameStep.Side currentSide = GameStep.Side.Black;

	/**
	 * 現在のステップ数
	 */
	private int currentStep = 0;

	/**
	 * currentState[6][6] = Side.White;
	 * 
	 * 現在の盤面の状態
	 */
	private GameStep.Side[][] currentBoard = new GameStep.Side[8][8];

	{
		// 何も置かれていないマスはnullではなくNoneが設定されている。
		for (int x = 0; x < currentBoard.length; x++) {
			for (int y = 0; y < currentBoard[x].length; y++) {
				currentBoard[x][y] = GameStep.Side.None;
			}
		}
		currentBoard[3][3] = GameStep.Side.White;
		currentBoard[3][4] = GameStep.Side.Black;
		currentBoard[4][3] = GameStep.Side.Black;
		currentBoard[4][4] = GameStep.Side.White;
	}

	/**
	 * ゲーム終了の状態を保持します。
	 */
	private boolean ended = false;

	/**
	 * <p>
	 * ゲーム盤上の変更を通知されるオブジェクト
	 * </p>
	 * <p>
	 * 当フィールドは永続化されません。
	 * </p>
	 */
	@Attribute(persistent = false)
	private List<Observer> observers = new ArrayList<Observer>();

	/**
	 * どちらのコマの色かを指定するコンストラクター
	 * 
	 * @param boardId
	 * @param mySide
	 */
	public GameBoard(String boardId, GameStep.Side mySide) {
		this.id = boardId;
		this.mySide = mySide;
	}

	/**
	 * 当ゲーム盤の識別子を返します。
	 * 
	 * @return ゲーム盤の識別子
	 */
	public String getId() {
		return this.id;
	}

	public GameStep.Side getMySide() {
		return mySide;
	}

	public GameStep.Side getCurrentSide() {
		return currentSide;
	}

	public int getCurrentStep() {
		return currentStep;
	}

	public GameStep.Side[][] getCurrentBoard() {
		return currentBoard;
	}

	/**
	 * 自分の手番かどうかを調べます。
	 * 
	 * @return true:自分の手番, false:相手の手番
	 */
	public boolean isMyTurn() {
		return this.mySide == currentSide;
	}

	/**
	 * ゲームが終了かどうかを判定します。
	 * 
	 * @return ゲーム終了時、true
	 */
	public boolean isEnded() {
		return ended;
	}

	/**
	 * コマを配置可能な場所のリストを返します。
	 */
	public List<Position> getPuttablePlaces() {
		// TODO
		return Collections.emptyList();
	}

	/**
	 * <p>
	 * 指定位置に指定色のコマを打った場合の状況を予測します。コマを打った場合の状況を適用するにはapplyを使用してください。
	 * </p>
	 * 指定位置にコマを打つことが可能だった場合、コマを打つ前後での差分の情報を返します。 コマを打つことが不可能だった場合、nullを返します。
	 * 
	 * @param x
	 *            x座標の位置
	 * @param y
	 *            y座標の位置
	 * @return コマを打つ前後での差分の情報。配置不可能の場合、null
	 */
	public GameStep calculateForPutOn(int x, int y) {
		return calculateForPutOn(this.mySide, x, y);
	}

	private GameStep calculateForPutOn(GameStep.Side side, int x, int y) {
		// ガード句、盤の大きさオーバー、石がすでに配置されている場合には配置不可能
		if (x < 0 || x >= this.currentBoard.length || y < 0
				|| y >= this.currentBoard[x].length) {
			return null;
		} else if (this.currentBoard[x][y] != GameStep.Side.None) {
			return null;
		}

		// 該当箇所に石を配置した場合に裏返る石をリストアップ
		List<StonePosition> changes = new ArrayList<StonePosition>();
		changes.addAll(listUpChanges(side, x, y, -1, 0));
		changes.addAll(listUpChanges(side, x, y, -1, -1));
		changes.addAll(listUpChanges(side, x, y, 0, -1));
		changes.addAll(listUpChanges(side, x, y, 1, -1));
		changes.addAll(listUpChanges(side, x, y, 1, 0));
		changes.addAll(listUpChanges(side, x, y, 1, 1));
		changes.addAll(listUpChanges(side, x, y, 0, 1));
		changes.addAll(listUpChanges(side, x, y, -1, 1));
		return (changes.isEmpty()) ? null : new GameStep(side, x, y,
				this.currentStep + 1);
	}

	/**
	 * 指定位置からの指定方向への変更される石のリストアップ
	 * 
	 * @param side
	 * @param startX
	 * @param startY
	 * @param directionX
	 * @param directionY
	 * @return
	 */
	private List<StonePosition> listUpChanges(GameStep.Side side, int startX,
			int startY, int directionX, int directionY) {

		// 指定方向に移動しながら変更となるコマをリストアップ
		// 端、空白、指定色と同じコマに行き当たったらストップ。
		// 前者の2パターンでは変更コマはクリアされる。
		List<StonePosition> changes = new ArrayList<StonePosition>();
		int currentX = startX, currentY = startY;
		for (int i = 0; i < 8; i++) {
			currentX += directionX;
			currentY += directionY;

			if (currentX < 0 || currentX >= this.currentBoard.length
					|| currentY < 0 || currentY >= this.currentBoard[0].length) {
				changes.clear();
				break;
			} else if (this.currentBoard[currentX][currentY] == GameStep.Side.None) {
				changes.clear();
				break;
			} else if (this.currentBoard[currentX][currentY] == side) {
				break;
			} else {
				changes.add(new StonePosition(currentX, currentY, side));
			}
		}
		return changes;
	}

	public void pass(GameStep.Side side) {

	}

	/**
	 * <p>
	 * ゲーム盤に指定されたステップ情報を適用します。
	 * </p>
	 * ゲーム盤の現在のステップ数とステップ数の整合性が一致していない場合、falseを返します。
	 * 
	 * @param step
	 * @return
	 */
	public boolean apply(GameStep newStep) {
		// ステップ数の確認
		if (this.currentStep + 1 != newStep.getStepNumber()) {
			return false;
		}

		// 石の変更のリストアップ
		final GameStep.Side side = newStep.getSide();
		final int x = newStep.getX(), y = newStep.getY();
		List<StonePosition> changes = new ArrayList<StonePosition>();
		changes.addAll(listUpChanges(side, x, y, -1, 0));
		changes.addAll(listUpChanges(side, x, y, -1, -1));
		changes.addAll(listUpChanges(side, x, y, 0, -1));
		changes.addAll(listUpChanges(side, x, y, 1, -1));
		changes.addAll(listUpChanges(side, x, y, 1, 0));
		changes.addAll(listUpChanges(side, x, y, 1, 1));
		changes.addAll(listUpChanges(side, x, y, 0, 1));
		changes.addAll(listUpChanges(side, x, y, -1, 1));
		changes.add(new StonePosition(x, y, side));

		// 変更の適用
		for (StonePosition change : changes) {
			this.currentBoard[change.x][change.y] = change.side;
		}
		this.currentStep++;
		this.currentSide = (newStep.getSide() == GameStep.Side.White) ? GameStep.Side.Black
				: GameStep.Side.White;

		// 相手側の置く場所がなければ、手番をさらに変更
		if (!canPutOn(this.currentSide)) {
			this.currentSide = (this.currentSide == GameStep.Side.White) ? GameStep.Side.Black
					: GameStep.Side.White;

			// さらに置く場所がない場合、ゲームセット
			if (!canPutOn(this.currentSide)) {
				this.ended = true;
			}
		}

		// オブザーバーへの変更の通知
		notifyUpdate();
		return true;
	}

	/**
	 * 指定された手番側で石を配置することが可能か確認する。
	 * 
	 * @param side
	 *            手番
	 * @return true:配置可能 false:配置不可能
	 */
	private boolean canPutOn(GameStep.Side side) {
		for (int x = 0; x < currentBoard.length; x++) {
			for (int y = 0; y < currentBoard[x].length; y++) {
				if (calculateForPutOn(side, x, y) != null) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * 当オブジェクトの内容の変更を通知される対象を登録します。
	 * 
	 * @param observer
	 *            内容の変更を通知される対象クラス
	 */
	public void addObserver(Observer observer) {
		this.observers.add(observer);
	}

	/**
	 * 全オブザーバーに変更を通知する。
	 */
	private void notifyUpdate() {
		for (Observer one : this.observers) {
			one.notifyUpdate(this);
		}
	}

	/**
	 * 石の位置を表すクラス
	 */
	private final class StonePosition {
		final int x, y;
		final GameStep.Side side;

		public StonePosition(int x, int y, GameStep.Side side) {
			this.x = x;
			this.y = y;
			this.side = side;
		}
	}
}
